﻿using System;
using System.Collections.Generic;
using Notoric.Commanding.Holders;

namespace Notoric.Commanding.CommandBase
{
    /// <summary>
    /// Extends the <see cref="CommandBase"/> class with support for undoable commands and command stacks management.
    /// </summary>
    public abstract class UndoableCommandBase : CommandBase
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="UndoableCommandBase"/> instance.
        /// <param name="undoStack">Reference to the undo stack</param>
        /// <param name="redoStack">Reference to the redo stack</param>
        /// </summary>
        public UndoableCommandBase(CommandStack undoStack, CommandStack redoStack)
        {
            ExecutionState = ExecutionState.NotExecuted;
            UseStacks = true;

            this.undoStack = undoStack;
            this.redoStack = redoStack;
        }

        #endregion

        #region ICommand Members

        /// <summary>
        /// Executes the command and moves it between the command stacks accordingly.
        /// </summary>
        /// <remarks>
        /// Based on the current <see cref="Notoric.Commanding.CommandBase.UndoableCommandBase.ExecutionState"/> the command is either:
        /// <list type="bullet">
        /// <item>
        /// Executed via a call to internal <see cref="ExecuteCommand"/> method (and then <see cref="CommandOperation"/>) and its <see cref="ExecutionState"/> is changed from 
        /// <see cref="F:Notoric.Commanding.CommandBase.ExecutionState.NotExecuted"/>
        /// to <see cref="F:Notoric.Commanding.CommandBase.ExecutionState.Executed"/>. The command is also put on the undo stack.
        /// </item>
        /// <item>
        /// Undone via a call to internal <see cref="UndoCommand"/> method (and then <see cref="UndoOperation"/>) and its 
        /// <see cref="Notoric.Commanding.CommandBase.UndoableCommandBase.ExecutionState"/> is changed from 
        /// <see cref="F:Notoric.Commanding.CommandBase.ExecutionState.Executed"/>
        /// to <see cref="F:Notoric.Commanding.CommandBase.ExecutionState.Undone"/>. The command is also put on the redo stack.
        /// </item>
        /// <item>
        /// Redone via a call to internal <see cref="RedoCommand"/> method (and then <see cref="RedoOperation"/>) and its 
        /// <see cref="Notoric.Commanding.CommandBase.UndoableCommandBase.ExecutionState"/> is changed from 
        /// <see cref="F:Notoric.Commanding.CommandBase.ExecutionState.Undone"/>
        /// to <see cref="F:Notoric.Commanding.CommandBase.ExecutionState.Executed"/>. The command is also put on the undo stack.
        /// </item>
        /// </list>
        /// </remarks>
        /// <param name="parameter">Parameter for the command execution. This value is ignored if the command is being undone or redone.</param>
        public override void Execute(object parameter)
        {
            if (!CanExecute(parameter))
                throw new ExecutionException("The command cannot execute, CanExecute returns false!");

            switch (ExecutionState)
            {
                case ExecutionState.NotExecuted:
                    try
                    {
                        ExecuteCommand(parameter);
                    }
                    catch (Exception e)
                    {
                        throw new ExecutionException("An unhandled exception prevented the command from executing correctly!", e);
                    }
                    ExecutionState = ExecutionState.Executed;
                    LastExecuted = DateTime.Now;
                    if (UseStacks)
                    {
                        if (undoStack != null)
                            undoStack.Push(this);
                        if (redoStack != null)
                            redoStack.Clear();
                    }
                    break;
                case ExecutionState.Executed:
                    try
                    {
                        UndoCommand();
                    }
                    catch (Exception e)
                    {
                        throw new ExecutionException("An unhandled exception prevented the command from being undone correctly!", e);
                    }
                    ExecutionState = ExecutionState.Undone;
                    LastExecuted = DateTime.Now;
                    if (UseStacks && undoStack != null && !undoStack.IsEmpty)
                    {
                        if (undoStack.Peek() == this)
                            undoStack.Pop();
                    }
                    if (UseStacks && redoStack != null)
                        redoStack.Push(this);
                    break;
                case ExecutionState.Undone:
                    try
                    {
                        RedoCommand();
                    }
                    catch (Exception e)
                    {
                        throw new ExecutionException("An unhandled exception prevented the command from being redone correctly!", e);
                    }
                    ExecutionState = ExecutionState.Executed;
                    LastExecuted = DateTime.Now;
                    if (UseStacks && redoStack != null && !redoStack.IsEmpty)
                    {
                        if (redoStack.Peek() == this)
                            redoStack.Pop();
                    }
                    if (UseStacks && undoStack != null)
                        undoStack.Push(this);
                    break;
            }

            RaiseCanExecuteChanged();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Binds the value of a given parameter to the value of a given parameter of a given undoable command.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This method allows an easy creation of macro commands. Binding the parameter values to subcommands results in creating
        /// an execution tree where the leaf commands (those whose values are not bound to other commands) are executed first.
        /// The order of execution is not defined for the commands at the same level of the tree.
        /// </para>
        /// <example>
        /// RootCommand(ParamA: 5, ParamB: Bound to SubCommand1.ParamB, ParamC: Bound to SubCommand2.ParamC)<br/>
        /// SubCommand1(Param A: Bound to SubCommand3.ParamA, ParamB: Bound to SubCommand3.ParamB)<br/>
        /// SubCommand2(ParamC: Result of the command execution)<br/>
        /// SubCommand3(ParamA, ParamB: Results of the command execution)<br/>
        /// <br/>
        /// Results in the follwoing order of execution:<br/>
        /// 1. SubCommand3<br/>
        /// 2. SubCommand1 and SubCommand2 in any order<br/>
        /// 3. RootCommand
        /// </example>
        /// <para>
        /// Undoable command can be bound only to other undoable commands.
        /// </para>
        /// </remarks>
        /// <param name="name">Name of the parameter whose value is being bound to a subcommand</param>
        /// <param name="command">Instance of the <see cref="UndoableCommandBase"/> class to which the value is being bound</param>
        /// <param name="outputName">Name of the parameter of the <paramref name="command"/> to wich the value is being bound</param>
        /// <exception cref="ParameterDoesNotExistException">
        /// The parameter being bound does not exist on this command or the target parameter does not exist on the other command.
        /// </exception>
        /// <exception cref="IncompatibleTypesException">
        /// The parameters being bound do not have compatible value types.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// Some of the parameters is null.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// The parameter beind bound (i.e. the one identified by <paramref name="name"/>) is output.
        /// </exception>
        public void BindParameterToCommand(string name, UndoableCommandBase command, string outputName)
        {
            BindParameterToCommandInternal(name, command, outputName);
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Executes the command.
        /// </summary>
        /// <remarks>
        /// If any parameter values of this command are bound to subcommands, the subcommands are executed before calling <see cref="CommandOperation"/> and they are added
        /// to the <see cref="executedSubCommands"/> list.
        /// </remarks>
        /// <param name="parameter">Parameter for the command execution</param>
        internal void ExecuteCommand(object parameter)
        {
            foreach (Parameter param in parameterSet)
            {
                if (param.ValueHolder.ValueState == ValueState.Unknown)
                {
                    if (param.Command != null && param.Command.CanExecute(parameter))
                    {
                        UndoableCommandBase cmd = (UndoableCommandBase)param.Command;
                        cmd.ExecuteCommand(parameter);
                        executedSubCommands.Add(cmd);
                    }
                }
            }

            CommandOperation(parameter);
        }

        /// <summary>
        /// Redoes the command actions that were previously undone.
        /// </summary>
        /// <remarks>
        /// First the commands from <see cref="executedSubCommands"/> list are redone, then this command <see cref="RedoOperation"/> is called.
        /// </remarks>
        internal void RedoCommand()
        {
            foreach (UndoableCommandBase cmd in executedSubCommands)
                cmd.RedoCommand();

            RedoOperation();
        }

        /// <summary>
        /// Undoes the command actions.
        /// </summary>
        /// <remarks>
        /// First the <see cref="UndoOperation"/> is called and then the commands in the <see cref="executedSubCommands"/> list are undone.
        /// </remarks>
        internal void UndoCommand()
        {
            UndoOperation();

            foreach (UndoableCommandBase cmd in executedSubCommands)
                cmd.UndoCommand();
        }

        #endregion

        #region Abstract Methods

        /// <summary>
        /// Implements the logic for executing this command.
        /// </summary>
        /// <param name="parameter">Parameter for the execution</param>
        protected abstract void CommandOperation(object parameter);

        /// <summary>
        /// Implements the logic for redoing this command after it was undone.
        /// </summary>
        protected abstract void RedoOperation();

        /// <summary>
        /// Implements the logic for undoing this command after it was executed.
        /// </summary>
        protected abstract void UndoOperation();

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the current execution state of this command.
        /// </summary>
        /// <value>
        /// <para>
        /// One of the <see cref="Notoric.Commanding.CommandBase.ExecutionState"/> values.
        /// </para>
        /// </value>
        public ExecutionState ExecutionState
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the undo stack used by this command.
        /// </summary>
        /// <value>
        /// <para>
        /// Undo stack on which this command is put when it executes.
        /// </para>
        /// </value>
        public CommandStack UndoStack
        {
            get { return undoStack; }
        }

        /// <summary>
        /// Gets the redo stack used by this command.
        /// </summary>
        /// <value>
        /// <para>
        /// Redo stack on which this command is put when it is undone.
        /// </para>
        /// </value>
        public CommandStack RedoStack
        {
            get { return redoStack; }
        }

        /// <summary>
        /// Gets or sets the value indicating whether the command should use the undo and redo stacks or not.<br/>
        /// Default is <see langword="true"/>.
        /// </summary>
        /// <value>
        /// <para>
        /// Indicates whether the command execution will modify the undo and redo stacks or not. If this value is <see langword="true"/>
        /// then the command is moved between the stacks as described with the <see cref="Execute"/> method. Otherwise the stacks
        /// are not modified.
        /// </para>
        /// </value>
        public bool UseStacks
        {
            get;
            set;
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// List of executed subcommands.
        /// </summary>
        private List<UndoableCommandBase> executedSubCommands = new List<UndoableCommandBase>();
        /// <summary>
        /// Holds a reference to the redo stack.
        /// </summary>
        private CommandStack redoStack;
        /// <summary>
        /// Holds a reference to the undo stack.
        /// </summary>
        private CommandStack undoStack;

        #endregion
    }

    /// <summary>
    /// Defines the execution state of an undoable command (<see cref="UndoableCommandBase"/>).
    /// </summary>
    public enum ExecutionState
    {
        /// <summary>
        /// The command was not executed yet.
        /// </summary>
        NotExecuted,
        /// <summary>
        /// The command was executed or redone.
        /// </summary>
        Executed,
        /// <summary>
        /// The command was undone.
        /// </summary>
        Undone
    }
}
